import sys
import os
import csv
from time import ctime
from random import random
from PyQt4 import QtCore, QtGui
import numpy as np
from ui.mainwindow import Ui_MainWindow
from simulate import ResSim
from measurement import Measurement
try:
    from daqImpact import daqImpact
except ImportError:
    pass
 
class MainApp(QtGui.QMainWindow):
    """
    Controller connecting the graphical user interface (ui.mainWindow) to
    the model (daq and impact_measurement). Inherits the QtGui.MainWindow
    class.
    Class variables:
    ui - Ui_MainWindows (user interface)
    daq - digital analog device
    """
    def __init__(self, parent=None):
        """ 
        Defines the class objects:
            daq interface 
            ui - user interface
        Defines a dictionary for the default values for the gui. Note that the
        dictionary keywords are specifically named in coordination with the
        graphical interface objects.
        """
        super(MainApp,self).__init__(parent) 
        try:
            self.daq = daqImpact()
        except NameError:
            self.daq = daqImpactDebug(self)
        gui_values = {
                'ExpDescription' : 'Impact test measurement on ' + ctime(),
                'SavePath' : '',
                'DropHeight':4.00,              # drop height (cm)
                'SphereRadius':4.76,            # sphere radius (mm)
                'SphereModulus':1.70,           # sphere modulus (GPa)
                'SurfaceModulus':2.20,          # plane modulus (GPa)
                'SpherePR':0.45,                # sphere Poisson's ratio
                'SurfacePR':0.37,               # plane Poisson's ratio
                'SphereMass':.401,              # sphere mass (g)
                'COR':0.79,                     # coefficient of restitution
                'Calibration':0.0055,            # calibration constant (nm/mV)
                'EstModulus':0.0,               # fit of plane modulus (GPa)
                'EstPoissonRatio':0.0,          # fit of plane Poisson's ratio
                'EstCOR':0.0,                   # fit of estimated COR
                'CrystalMass':35,               # mass of the crystal/compact/load cell (g)
                'CrystalStiffness':0.42,         # stiffness of the spring (GN/m)
                'CrystalDamping':0.55            # damping factor of the crystal (kN s/m)
                        }
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.set_gui_values(gui_values)
        self.setup_plot()
        self.define_connections()
        self.set_validators()
        self.connect_to_daq()

    def set_gui_values(self,gui_values):
        """
        Set the gui values using the dictionary from function argument
        gui_values - dictionary of gui values to set
        """
        self.ui.checkBoxArm.setChecked(False)

        for k, v in gui_values.iteritems():
            try:
                getattr(self.ui,'lineEdit%s'%(k)).setText(QtCore.QString(str(v)))
            except AttributeError:
                try:
                    getattr(self.ui,'lcdNumber%s'%(k)).display(QtCore.QString(str(v)))
                except AttributeError as e:
                    raise e

        #self.ui.lineEditCalibration.setReadOnly(True)

    def get_gui_values(self):
        """ 
        Retrieves the gui values from all lineEdit and lcdNumber user
        interface items.
        return dictionary
        """
        gui_values = {}

        for widget in dir(self.ui):
            if widget.find('lineEdit') != -1:
                gui_values[widget[8:]] = getattr(self.ui,widget).text()
            elif widget.find('lcdNumber') != -1:
                gui_values[widget[9:]] = getattr(self.ui,widget).value()

        for k, v in gui_values.iteritems():
            try:
                gui_values.update({k:float(v)})
            except ValueError:
                gui_values.update({k:str(v)})

        return gui_values

    def setup_plot(self):
        """
        Set up the plot
        """
        time_axis = np.linspace(0,1.0e3,1000)
        voltage = np.zeros((1000))
        self.line_measure = self.ui.widgetPlot.canvas.ax.plot(time_axis,voltage,label='measure')[0]
        self.line_measure.set_color('blue')
        self.line_measure.set_linestyle('-')
        self.plot_data(time_axis,voltage,self.line_measure)
        self.line_theory = self.ui.widgetPlot.canvas.ax.plot(time_axis,voltage,label='theory')[0]
        self.line_theory.set_color('red')
        self.line_theory.set_linestyle('-')
        self.plot_data(time_axis,voltage,self.line_theory)
        self.line_fit = self.ui.widgetPlot.canvas.ax.plot(time_axis,voltage,label='fit')[0]
        self.line_fit.set_color('green')
        self.line_fit.set_linestyle('-')
        self.plot_data(time_axis,voltage,self.line_fit)
        """
        alternative approach
        self.line_measure = lines.Line2D(np.array([0]),np.array([0]),linestyle='-',color='blue',label='measure')
        self.line_theory = lines.Line2D(np.array([0]),np.array([0]),linestyle='-',color='red',label='simulate')
        self.line_fit = lines.Line2D(np.array([0]),np.array([0]),linestyle='-',color='green',label='fit')
        """

    def define_connections(self):
        """
        Defines the slots for signals from the user interface widgets
        """
        QtCore.QObject.connect(self.ui.checkBoxArm, QtCore.SIGNAL('clicked()'), self.arm_daq)
        QtCore.QObject.connect(self.ui.pushButtonLoadFile, QtCore.SIGNAL('clicked()'), self.load_file)
        QtCore.QObject.connect(self.ui.pushButtonSimulate, QtCore.SIGNAL('clicked()'), self.simulate)
        QtCore.QObject.connect(self.ui.pushButtonFit, QtCore.SIGNAL('clicked()'), self.fit)
        QtCore.QObject.connect(self.ui.pushButtonClearPlot, QtCore.SIGNAL('clicked()'), self.clear_plot)
        QtCore.QObject.connect(self.ui.pushButtonSavePath, QtCore.SIGNAL('clicked()'), self.save_data)

    def define_validators(self):
        """
        Defines the validator for the user interface widgets.
        The validator requires that the user input be of numeric characters
        only.
        """
        validators = {
            "Calibration" : QtGui.QDoubleValidator(0,100,4,None),
            "DropHeight" : QtGui.QDoubleValidator(0,10,2,None),
            "CrystalMass" : QtGui.QDoubleValidator(0,1000,2,None),
            "CrystalStiffness" : QtGui.QDoubleValidator(0,20,2,None),
            "CrystalDamping" : QtGui.QDoubleValidator(0,10,2,None),
            "SphereMass" : QtGui.QDoubleValidator(0,5,3,None),
            "SphereRadius" : QtGui.QDoubleValidator(0,5,3,None),
            "SphereModulus" : QtGui.QDoubleValidator(0,200,3,None),
            "SpherePR" : QtGui.QDoubleValidator(0,0.5,3,None),
            "SurfaceModulus" : QtGui.QDoubleValidator(0,200,3,None),
            "SurfacePR" : QtGui.QDoubleValidator(0,0.5,3,None),
            "COR" : QtGui.QDoubleValidator(0,1.0,3,None),
                    }
        return validators

    def set_validators(self):
        """
        Set the validators for the lineEdit widgets
        """
        validators = self.define_validators()
        for k, v in validators.iteritems():
            getattr(self.ui,'lineEdit%s'%(k)).setValidator(v)

    def connect_to_daq(self):
        """
        Attempt to connect to the daq
        """
        self.daq.setup()

    def validate_gui_values(self):
        """
        Checks that all the user input is numeric and within the expected
        range.  If errors are detected, it highlights the objectionaly input
        box values.
        return errors - boolean (True if errors, False if no errors)
        """
        errors = False

        validators = self.define_validators()
        for k, v in validators.iteritems():
            lineEdit = getattr(self.ui,'lineEdit%s'%(k))
            if v.validate(lineEdit.text(),0)[0] != 2:
                lineEdit.selectAll()
                errors = True

        return errors

    def arm_daq(self):
        """
        Slot for the arm_daq checkbox signal.
        If the box is checked (daq is armed)
            disarm the daq and uncheck the box and return
        If no errors in the user parameter inputs
            Try to perform acquisition
            If succesful:
                read the buffer data signal (mV)
                zero the signal
                plot the signal (mV)
        """
        checked = self.ui.checkBoxArm.isChecked()
        # Stop acquisition if user is unchecking box
        if checked == False:
            self.daq.stop_acquisition()

        errors = self.validate_gui_values()
        if errors == False:
            [status, time_axis, buffer_data] = self.daq.perform_acquisition()
            if status == True:
                signal = buffer_data - np.average(buffer_data[-200:])
                time_axis *= 1.0e6
                self.plot_data(time_axis, signal, self.line_measure)
        self.ui.checkBoxArm.setChecked(False)
            
    def plot_data(self,time_axis,deflection,line):
        """
        Plot the data into the matplotlib widget
        time_axis - numpy array of signal time axis (seconds)
        deflection - numpy array of crystal deflection (um)
        line - matplotlib line object
        """
        line.set_data(time_axis,deflection)
        self.ui.widgetPlot.canvas.ax.relim()
        self.ui.widgetPlot.canvas.ax.autoscale_view(scalex=True,scaley=True)
        self.ui.widgetPlot.canvas.ax.set_xlim(0,500)
        self.ui.widgetPlot.canvas.draw()

    def simulate(self):
        """
        Calculate the theoretical crystal signal from the Hertzian force
        """
        sim = ResSim()
        errors = self.validate_gui_values()
        if errors == False:
            gui_vals = self.get_gui_values()
            params = {'h':gui_vals['DropHeight'] * 1.0e-2,
                      'm':gui_vals['SphereMass'] * 1.0e-3,
                      'R':gui_vals['SphereRadius'] * 1.0e-3,
                      'Es':gui_vals['SphereModulus'] * 1.0e9,
                      'Ep':gui_vals['SurfaceModulus'] * 1.0e9,
                      'vs':gui_vals['SpherePR'],
                      'vp':gui_vals['SurfacePR'],
                      'COR':gui_vals['COR'],
                      'm_crystal':gui_vals['CrystalMass'] * 1e-3,
                      'k_crystal':gui_vals['CrystalStiffness'] * 1e9,
                      'mu_dashpot':gui_vals['CrystalDamping'] * 1e3}
            sim.set_parameters(params)
            [t,y,f] = sim.solve()

            t *= 1.0e6
            signal = np.zeros(3000)
            cal = gui_vals['Calibration'] * 1.0e-9
            signal[1000:2000] = y[:,2] / cal
            maximum = np.argmax(signal)
            # Adjust curve to have maximum at 100us and be 1000us long
            signal = signal[maximum-100:maximum+900]
            self.plot_data(t,signal,self.line_theory)

    def fit(self):
        """
        Fit the experimental data
        """
        meas = Measurement()
        sim = ResSim()
        exp_data = self.line_measure.get_data()
        gui_vals = self.get_gui_values()
        exp_params = {'h':gui_vals['DropHeight'] * 1.0e-2,
                  'm':gui_vals['SphereMass'] * 1.0e-3,
                  'R':gui_vals['SphereRadius'] * 1.0e-3,
                  'Es':gui_vals['SphereModulus'] * 1.0e9,
                  'vs':gui_vals['SpherePR'],
                  'cal' : gui_vals['Calibration'] * 1.0e-9,
                  'm_crystal':gui_vals['CrystalMass'] * 1e-3,
                  'k_crystal':gui_vals['CrystalStiffness'] * 1e9,
                  'mu_dashpot':gui_vals['CrystalDamping'] * 1e3}
        est_params = {'Ep':gui_vals['SurfaceModulus'] * 1.0e9,
                  'vp':gui_vals['SurfacePR'],
                  'COR':gui_vals['COR']}

        meas.set_data(exp_data[0] * 1.0e-6, exp_data[1])
        meas.set_parameters(exp_params)
        meas.set_parameters(est_params)
        meas.fit_voltage()

        fit_params = meas.get_estimates()
        params = dict(exp_params.items() + fit_params.items())
        sim.set_parameters({i:params[i] for i in ['h','R','Es','Ep','vs',
            'vp','m', 'COR','m_crystal','k_crystal','mu_dashpot']})
        [t,y,f] = sim.solve()

        t *= 1.0e6
        signal = np.zeros(3000)
        cal = gui_vals['Calibration'] * 1.0e-9
        signal[1000:2000] = y[:,2] / cal
        maximum = np.argmax(signal)
        # Adjust curve to have maximum at 100us and be 1000us long
        signal = signal[maximum-100:maximum+900]
        self.plot_data(t,signal,self.line_fit)

        # TODO R2
        params = {}
        params.update({'EstModulus':round(fit_params['Ep'] * 1.0e-9,2), 
                        'EstPoissonRatio':round(fit_params['vp'],2),
                        'EstCOR':round(fit_params['COR'],2)})
        self.set_gui_values(params)

    def clear_plot(self):
        """
        Clear all the lines plots in the figure
        """
        self.ui.widgetPlot.canvas.ax.clear()
        self.setup_plot()
        self.ui.widgetPlot.canvas.draw()
 
    def save_data(self):
        """
        Save the plot data to a text file
        """
        errors = self.validate_gui_values()
        gui_vals = self.get_gui_values()
        fullpath = gui_vals['SavePath']
        [path,fname] = os.path.split(fullpath)
        
        if not (errors or os.path.exists(fullpath)) and os.path.isdir(path):
            exp_data = self.line_measure.get_data()
            sim_data = self.line_theory.get_data()
            fit_data = self.line_fit.get_data()
            data = np.array([exp_data[0],exp_data[1],sim_data[0],sim_data[1],
                             fit_data[0],fit_data[1]])

            with open(fullpath,mode='w') as fObj:
                fObj.write('****Begin Parameter Values****\n')
                for k, v in gui_vals.iteritems():
                    fObj.write('%s =:= %s \n'%(k,v))
                fObj.write('****End Parameter Values****\n')
                fObj.write('****Begin Data****\n')
                fObj.write('Measured Time, Measured Voltage, Simulated Time,' 
                        'Simulated Voltage, Fit Time, Fit Voltage\n')
                csv_writer = csv.writer(fObj)
                csv_writer.writerows(data.transpose())
                fObj.write('****End Data****\n')

            self.ui.lineEditSavePath.setText(QtCore.QString(path))

        else:
            self.ui.lineEditSavePath.setText(QtCore.QString(fullpath +
                '(check filename and path)'))
            self.ui.lineEditSavePath.selectAll()

    def load_file(self):
        """
        Load a previously saved file
        """
        loadfile = QtCore.QString()
        dialogcaption = QtCore.QString('Select previous measurement file')
        loadfile.swap(QtGui.QFileDialog.getOpenFileName(caption=dialogcaption))

        gui_vals = {}
        data = []
        lineNo = {'Begin Parameter':0,'End Parameter':0,
                  'Begin Data':0, 'End Data':0}

        with open(loadfile,mode='r') as f:
            for ln, txt in enumerate(f):
                if txt.find("****Begin Parameter Values****") != -1:
                    lineNo['Begin Parameter'] = ln
                elif txt.find("****End Parameter Values****") != -1: 
                    lineNo['End Parameter'] = ln
                elif txt.find("****Begin Data****") != -1: 
                    lineNo['Begin Data'] = ln + 1
                elif txt.find("****End Data****") != -1: 
                    lineNo['End Data'] = ln
                
            f.seek(0)
            csv_f = csv.reader(f)
            for ln, txt in enumerate(csv_f):
                if (ln > lineNo['Begin Parameter'] and 
                    ln < lineNo['End Parameter']):
                    k, v = txt[0].split("=:=")
                    try:
                        k, v = k.strip(), float(v.strip())
                    except ValueError:
                        k, v = k.strip(), str(v.strip())
                    gui_vals.update({k : v})
                elif (ln > lineNo['Begin Data'] and 
                      ln < lineNo['End Data']):
                    data.append(txt)

        self.set_gui_values(gui_vals)
        data = np.array(data,dtype='f8').transpose()
        self.plot_data(data[0],data[1],self.line_measure)
        self.plot_data(data[2],data[3],self.line_theory)
        self.plot_data(data[4],data[5],self.line_fit)

class daqImpactDebug(object):
    """
    Module to simulate daq interaction for debugging
    """
    def __init__(self,main_app):
        self.gui = main_app

    def  setup(self):
        pass

    def perform_acquisition(self):
        sim = ResSim()
        gui_vals = self.gui.get_gui_values()
        y = lambda x: 0.4 * x + 0.8
        params = {'h':gui_vals['DropHeight'] * 1.0e-2 * y(random()),
                  'm':gui_vals['SphereMass'] * 1.0e-3 * y(random()),
                  'R':gui_vals['SphereRadius'] * 1.0e-3 * y(random()),
                  'Es':gui_vals['SphereModulus'] * 1.0e9 * y(random()),
                  'Ep':gui_vals['SurfaceModulus'] * 1.0e9 * y(random()),
                  'vs':gui_vals['SpherePR'] * y(random()),
                  'vp':gui_vals['SurfacePR'] * y(random()),
                  'COR':gui_vals['COR'] * y(random()),
                  'm_crystal':gui_vals['CrystalMass'] * 1e-3,
                  'k_crystal':gui_vals['CrystalStiffness'] * 1e9,
                  'mu_dashpot':gui_vals['CrystalDamping'] * 1e3}
        sim.set_parameters(params)
        [t,y,f] = sim.solve()

        signal = np.zeros(3000)
        cal = gui_vals['Calibration'] * 1.0e-9
        signal[1000:2000] = y[:,2] / cal
        maximum = np.argmax(signal)
        # Adjust curve to have maximum at 100us and be 1000us long
        signal = signal[maximum-100:maximum+900]
        return [True, t, signal]

    def stop_acquisition(self):
        pass

def debug_trace():
    """Set a tracepoint in the Python debugger that works with Qt"""
    from PyQt4.QtCore import pyqtRemoveInputHook
    from pdb import set_trace
    pyqtRemoveInputHook()
    set_trace()

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    myapp = MainApp()
    myapp.show()
    sys.exit(app.exec_())
